package org.zebra.mybatis.plus.ext.audit;

import cn.hutool.core.lang.Dict;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import io.swagger.annotations.ApiModelProperty;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.zebra.mybatis.plus.ext.audit.impl.DeleteDataAudit;
import org.zebra.mybatis.plus.ext.audit.impl.InsertDataAudit;
import org.zebra.mybatis.plus.ext.audit.impl.UpdateDataAudit;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * <p>
 * 数据变动记录插件
 * 默认会生成一条log，格式：
 * ----------------------INSERT LOG------------------------------
 * </p>
 * &lt;code&gt;
 * {
 * "tableName": "h2user",
 * "operation": "insert",
 * "recordStatus": "true",
 * "changedData": [
 * {
 * "LAST_UPDATED_DT": "null-&lt;2022-08-22 18:49:16.512",
 * "TEST_ID": "null-&lt;1561666810058739714",
 * "AGE": "null-&lt;THREE"
 * }
 * ],
 * "cost(ms)": 0
 * }
 * &lt;/code&gt;
 * <p>
 * * ----------------------UPDATE LOG------------------------------
 * <p>
 * &lt;code&gt;
 * {
 * "tableName": "h2user",
 * "operation": "update",
 * "recordStatus": "true",
 * "changedData": [
 * {
 * "TEST_ID": "102",
 * "AGE": "2-&lt;THREE",
 * "FIRSTNAME": "DOU.HAO-&lt;{\"json\":\"abc\"}",
 * "LAST_UPDATED_DT": "null-&lt;2022-08-22 18:49:16.512"
 * }
 * ],
 * "cost(ms)": 0
 * }
 * &lt;/code&gt;
 *
 * @author zhanghongbin
 */
public class DataAuditInterceptor implements InnerInterceptor {

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final Set<String> ignoredTable = new HashSet<>();

    private final Map<String, Set<String>> ignoredTableColumns = new ConcurrentHashMap<>();

    // 批量更新上限, 默认一次最多1000条
    private int BATCH_UPDATE_LIMIT = 1000;
    private boolean batchUpdateLimitationOpened = false;

    private final Map<String, Integer> BATCH_UPDATE_LIMIT_MAP = new ConcurrentHashMap<>(); // 表名->批量更新上限

    private DataAuditFieldTranslation dataAuditFieldTranslation = null;

    public void setDataAuditFieldTranslation(DataAuditFieldTranslation dataAuditFieldTranslation) {
        this.dataAuditFieldTranslation = dataAuditFieldTranslation;
    }

    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
        MappedStatement ms = mpSh.mappedStatement();
        final BoundSql boundSql = mpSh.boundSql();
        SqlCommandType sct = ms.getSqlCommandType();
        if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
            PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
            OperationResult operationResult;
            TableInfo tableInfo;
            long startTs = System.currentTimeMillis();
            try {
                Statement statement = JsqlParserGlobal.parse(mpBs.sql());
                if (statement instanceof Update) {
                    Update update = (Update) statement;
                    if (ignoredTable.contains(update.getTable().getName())) {
                        return;
                    }
                    tableInfo = TableInfoHelper.getTableInfo(update.getTable().getFullyQualifiedName());
                    operationResult = new UpdateDataAudit(
                                    this.ignoredTableColumns,
                                    this.BATCH_UPDATE_LIMIT,
                                    this.batchUpdateLimitationOpened,
                                    this.BATCH_UPDATE_LIMIT_MAP,
                                    update,
                                    ms,
                                    boundSql,
                                    connection,
                                    tableInfo)
                            .process();
                } else if (statement instanceof Delete) {
                    Delete delete = (Delete) statement;
                    if (ignoredTable.contains(delete.getTable().getName().toUpperCase())) {
                        return;
                    }
                    tableInfo = TableInfoHelper.getTableInfo(delete.getTable().getFullyQualifiedName());
                    operationResult = new DeleteDataAudit(
                                    this.ignoredTableColumns,
                                    this.BATCH_UPDATE_LIMIT,
                                    this.batchUpdateLimitationOpened,
                                    this.BATCH_UPDATE_LIMIT_MAP,
                                    delete,
                                    ms,
                                    boundSql,
                                    connection,
                                    tableInfo)
                            .process();
                } else {
                    logger.info("other operation sql={}", mpBs.sql());
                    return;
                }
            } catch (Exception e) {
                if (e instanceof DataUpdateLimitationException) {
                    throw (DataUpdateLimitationException) e;
                }
                logger.error("Unexpected error for mappedStatement={}, sql={}", ms.getId(), mpBs.sql(), e);
                return;
            }
            long costThis = System.currentTimeMillis() - startTs;
            if (operationResult != null) {
                operationResult.setCost(costThis);
                dealOperationResult(operationResult, tableInfo);
            }
        }
    }

    protected void dealOperationResult(OperationResult operationResult, TableInfo tableInfo) {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        Dict logDic =
                (Dict) Objects.requireNonNull(requestAttributes).getAttribute("$log", RequestAttributes.SCOPE_REQUEST);
        if (logDic == null) return;
        String operation = operationResult.getOperation();
        int type = 0;
        if (operation.equals("insert")) {
            type = 1;
        } else if (operation.equals("delete")) {
            type = 3;
        } else if (operation.equals("update")) {
            type = 2;
        }
        List<Dict> authList = logDic.get("audit", new ArrayList());
        logDic.set("audit", authList);
        Dict authDict = Dict.create();
        authList.add(authDict);
        authDict.set("dataAuditOperation", type)
                .set("dataAuditTableName", operationResult.getTableName())
                .set("dataAuditChangedData", operationResult.getChangedData());
        try {
            Class<?> entityClass;
            if (tableInfo != null) {
                entityClass = tableInfo.getEntityType();
            } else {
                entityClass = DataAuditInterceptor.class;
            }
            JSONArray jsonArray = JSONUtil.parseArray(operationResult.getChangedData());
            StringBuilder stringBuilder = new StringBuilder();
            jsonArray.forEach(obj -> {
                JSONObject jsonObject = (JSONObject) obj;
                Set<String> columnNameSet = jsonObject.keySet();
                columnNameSet.forEach(key -> {
                    try {
                        ApiModelProperty apiModelProperty = cn.hutool.core.util.ReflectUtil.getField(
                                        entityClass, StringUtils.underlineToCamel(key))
                                .getAnnotation(ApiModelProperty.class);
                        String[] content = jsonObject.getStr(key).split("->");
                        if (operationResult.getOperation().equals("update")) {
                            this.update(operationResult.getTableName(), stringBuilder, key, apiModelProperty, content);
                        } else if (operationResult.getOperation().equals("delete")) {
                            this.delete(
                                    operationResult.getTableName(), stringBuilder, key, apiModelProperty, content[0]);
                        } else {
                            this.insert(operationResult.getTableName(), stringBuilder, key, apiModelProperty, content);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
            });
            authDict.set("dataAuditTitle", stringBuilder.toString());
            Objects.requireNonNull(requestAttributes).setAttribute("$log", logDic, RequestAttributes.SCOPE_REQUEST);
        } catch (Exception e) {
            logger.error("dealOperationResult is error ", e);
        }
    }

    protected void delete(
            String tableName,
            StringBuilder stringBuilder,
            String key,
            ApiModelProperty apiModelProperty,
            String content) {
        String info;
        if (this.dataAuditFieldTranslation != null) {
            info = this.dataAuditFieldTranslation.translate(tableName, key, content);
        } else {
            info = content;
        }
        info = info.replaceAll("null", "空");
        if (apiModelProperty != null) {
            String value = apiModelProperty.value();
            stringBuilder.append(value + "原值为：" + info).append("&nbsp;");
        } else {
            stringBuilder.append(key + "原值为：" + info).append("&nbsp;");
        }
    }

    protected void insert(
            String tableName,
            StringBuilder stringBuilder,
            String key,
            ApiModelProperty apiModelProperty,
            String[] content) {
        String info;
        if (this.dataAuditFieldTranslation != null) {
            info = this.dataAuditFieldTranslation.translate(tableName, key, content[1]);
        } else {
            info = content[1];
        }
        info = info.replaceAll("null", "空");
        if (apiModelProperty != null) {
            String value = apiModelProperty.value();
            stringBuilder.append(value + "新值为：" + info).append("&nbsp;");
        } else {
            stringBuilder.append(key + "新值为：" + info).append("&nbsp;");
        }
    }

    protected void update(
            String tableName,
            StringBuilder stringBuilder,
            String key,
            ApiModelProperty apiModelProperty,
            String[] content) {
        String oldValue = content[0];
        String newValue = "";
        if (content.length == 2) {
            newValue = content[1];
        }
        if (this.dataAuditFieldTranslation != null) {
            oldValue = this.dataAuditFieldTranslation.translate(tableName, key, content[0]);
            if (!newValue.isEmpty()) {
                newValue = this.dataAuditFieldTranslation.translate(tableName, key, content[1]);
            }
        }
        oldValue = oldValue.replaceAll("null", "空");
        newValue = newValue.replaceAll("null", "空");
        if (apiModelProperty != null) {
            key = apiModelProperty.value();
        }
        if (newValue.isEmpty()) {
            stringBuilder.append(key + "原值为：" + oldValue).append("&nbsp;");
        } else {
            stringBuilder
                    .append(key + "原值为：" + oldValue)
                    .append("&nbsp;新值为：" + newValue)
                    .append("&nbsp;");
        }
    }

    /**
     * 判断哪些SQL需要处理
     * 默认INSERT/UPDATE/DELETE语句
     *
     * @param sql sql
     * @return true, false
     */
    protected boolean allowProcess(String sql) {
        String sqlTrim = sql.trim().toUpperCase();
        return sqlTrim.startsWith("INSERT") || sqlTrim.startsWith("UPDATE") || sqlTrim.startsWith("DELETE");
    }

    private String buildParameterObject(BoundSql boundSql) {
        Object paramObj = boundSql.getParameterObject();
        StringBuilder sb = new StringBuilder();
        sb.append("{");
        if (paramObj instanceof Map) {
            Map<String, Object> paramMap = (Map<String, Object>) paramObj;
            int index = 1;
            boolean hasParamIndex = false;
            String key;
            while (paramMap.containsKey((key = "param" + index))) {
                Object paramIndex = paramMap.get(key);
                sb.append("\"")
                        .append(key)
                        .append("\"")
                        .append(":")
                        .append("\"")
                        .append(paramIndex)
                        .append("\"")
                        .append(",");
                hasParamIndex = true;
                ++index;
            }
            if (hasParamIndex) {
                sb.delete(sb.length() - 1, sb.length());
                sb.append("}");
                return sb.toString();
            }
            for (Map.Entry<String, Object> ety : paramMap.entrySet()) {
                sb.append("\"")
                        .append(ety.getKey())
                        .append("\"")
                        .append(":")
                        .append("\"")
                        .append(ety.getValue())
                        .append("\"")
                        .append(",");
            }
            sb.delete(sb.length() - 1, sb.length());
            sb.append("}");
            return sb.toString();
        }
        sb.append("param:").append(paramObj);
        sb.append("}");
        return sb.toString();
    }

    /**
     * 设置批量更新记录条数上限
     *
     * @param limit 大小
     * @return 对象
     */
    public DataAuditInterceptor setBatchUpdateLimit(int limit) {
        this.BATCH_UPDATE_LIMIT = limit;
        return this;
    }

    public DataAuditInterceptor openBatchUpdateLimitation() {
        this.batchUpdateLimitationOpened = true;
        return this;
    }

    public DataAuditInterceptor configTableLimitation(String tableName, int limit) {
        this.BATCH_UPDATE_LIMIT_MAP.put(tableName.toUpperCase(), limit);
        return this;
    }

    /**
     * ignoredColumns = TABLE_NAME1.COLUMN1,COLUMN2; TABLE2.COLUMN1,COLUMN2; TABLE3.*; *.COLUMN1,COLUMN2
     * 多个表用分号分隔
     * TABLE_NAME1.COLUMN1,COLUMN2 : 表示忽略这个表的这2个字段
     * TABLE3.*: 表示忽略这张表的INSERT/UPDATE，delete暂时还保留
     * *.COLUMN1,COLUMN2:表示所有表的这个2个字段名都忽略
     *
     * @param properties 对象
     */
    @Override
    public void setProperties(Properties properties) {
        this.ignoredTable.add("sys_operation_log".toUpperCase());
        this.ignoredTable.add("sys_audit_log".toUpperCase());
        String ignoredColumns = properties.getProperty("ignoredColumns");
        if (ignoredColumns == null || ignoredColumns.trim().isEmpty()) {
            return;
        }
        String[] array = ignoredColumns.split(";");
        for (String table : array) {
            int index = table.indexOf('.');
            if (index == -1) {
                logger.error(
                        "invalid data={} for ignoredColumns, format should be TABLE_NAME1.COLUMN1,COLUMN2; TABLE2.COLUMN1,COLUMN2;",
                        table);
                continue;
            }
            String[] contentArray = table.split("\\.");
            if (contentArray.length < 2) continue;
            if (contentArray[1].equals("*")) {
                this.ignoredTable.add(contentArray[0].toUpperCase());
            } else {
                String tableName = table.substring(0, index).trim();
                String[] columnArray = table.substring(index + 1).split(",");
                Set<String> columnSet = new HashSet<>(columnArray.length);
                for (String column : columnArray) {
                    column = column.trim();
                    if (column.isEmpty()) {
                        continue;
                    }
                    columnSet.add(column.toUpperCase());
                }
                ignoredTableColumns.put(tableName.toUpperCase(), columnSet);
            }
        }
    }

    /**
     * 增加单独处理(支持批量增加)
     * 注：如果在 beforePrepare 中处理增加，不支持批量增加
     *
     * @param executor  对象
     * @param ms        对象
     * @param parameter 对象
     * @throws SQLException 异常
     */
    public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
        SqlCommandType sct = ms.getSqlCommandType();
        if (sct == SqlCommandType.INSERT) {
            BoundSql boundSql = ms.getBoundSql(parameter);
            PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
            OperationResult operationResult;
            TableInfo tableInfo;
            long startTs = System.currentTimeMillis();
            try {
                Statement statement = JsqlParserGlobal.parse(mpBs.sql());
                if (statement instanceof Insert) {
                    Insert insert = (Insert) statement;
                    if (ignoredTable.contains(insert.getTable().getName().toUpperCase())) {
                        return;
                    }
                    tableInfo = TableInfoHelper.getTableInfo(insert.getTable().getFullyQualifiedName());
                    operationResult =
                            new InsertDataAudit(this.ignoredTableColumns, insert, boundSql, tableInfo).process();
                } else {
                    logger.info("other operation sql={}", mpBs.sql());
                    return;
                }
            } catch (Exception e) {
                if (e instanceof DataUpdateLimitationException) {
                    throw (DataUpdateLimitationException) e;
                }
                logger.error("Unexpected error for mappedStatement={}, sql={}", ms.getId(), mpBs.sql(), e);
                return;
            }
            long costThis = System.currentTimeMillis() - startTs;
            if (operationResult != null) {
                operationResult.setCost(costThis);
                dealOperationResult(operationResult, tableInfo);
            }
        }
    }
}
